package study.mark.reflect;

import org.openjdk.jmh.annotations.*;
import org.openjdk.jmh.runner.Runner;
import org.openjdk.jmh.runner.RunnerException;
import org.openjdk.jmh.runner.options.Options;
import org.openjdk.jmh.runner.options.OptionsBuilder;

import java.lang.invoke.MethodHandle;
import java.lang.invoke.MethodHandles;
import java.lang.invoke.MethodType;
import java.lang.reflect.Method;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.TimeUnit;

@BenchmarkMode(Mode.AverageTime)
@Warmup(iterations = 2, time = 3)
@Measurement(iterations = 3, time = 5)
@Fork(1)
@State(value = Scope.Benchmark)
@OutputTimeUnit(TimeUnit.NANOSECONDS)
public class ReflectTest {

    private Method noParameterTestMethod;
    private MethodHandle noParameterTestMethodHandle;

    private Method oneObjectParameterMethod;
    private MethodHandle oneObjectParameterMethodHandle;

    private Method onePrimateParameterMethod;
    private MethodHandle onePrimateParameterMethodHandle;

    private Method twoObjectParameterMethod;
    private MethodHandle twoObjectParameterMethodHandle;

    private Method twoPrimateParameterMethod;
    private MethodHandle twoPrimateParameterMethodHandle;

    private Method threeObjectParameterMethod;
    private MethodHandle threeObjectParameterMethodHandle;

    private Method threePrimateParameterMethod;
    private MethodHandle threePrimateParameterMethodHandle;

    private Method override1Method1;
    private MethodHandle override1MethodHandle1;

    private Method override1Method2;
    private MethodHandle override1MethodHandle2;

    private Method override1Method3;
    private MethodHandle override1MethodHandle3;

    private Method genericMethod;
    private MethodHandle genericMethodHandle;

    private TestClass instance;

    private TestInterface interfaceInstance;
    private Method oneParameterInterfaceMethod;
    private MethodHandle oneParameterInterfaceMethodHandle;

    private Map<String, Object> map = new HashMap<>();

    @Setup
    public void setup() throws Exception {
        instance = new TestClass();
        noParameterTestMethod = TestClass.class.getMethod("noParameter");
        MethodHandles.Lookup lookup = MethodHandles.lookup();
        noParameterTestMethodHandle = lookup.findVirtual(TestClass.class,
                                                         "noParameter",
                                                         MethodType.methodType(String.class));

        oneObjectParameterMethod = TestClass.class.getMethod("oneObjectParameter", String.class);
        oneObjectParameterMethodHandle = lookup.findVirtual(TestClass.class,
                                                            "oneObjectParameter",
                                                            MethodType.methodType(String.class, String.class));

        onePrimateParameterMethod = TestClass.class.getMethod("onePrimateParameter", int.class);
        onePrimateParameterMethodHandle = lookup.findVirtual(TestClass.class,
                                                             "onePrimateParameter",
                                                             MethodType.methodType(int.class, int.class));

        twoObjectParameterMethod = TestClass.class.getMethod("twoObjectParameter", String.class, String.class);
        twoObjectParameterMethodHandle = lookup.findVirtual(TestClass.class,
                                                            "twoObjectParameter",
                                                            MethodType.methodType(String.class,
                                                                                  String.class,
                                                                                  String.class));

        twoPrimateParameterMethod = TestClass.class.getMethod("twoPrimateParameter", int.class, int.class);
        twoPrimateParameterMethodHandle = lookup.findVirtual(TestClass.class,
                                                             "twoPrimateParameter",
                                                             MethodType.methodType(int.class, int.class, int.class));

        threeObjectParameterMethod = TestClass.class.getMethod("threeObjectParameter",
                                                               String.class,
                                                               String.class,
                                                               String.class);
        threeObjectParameterMethodHandle = lookup.findVirtual(TestClass.class,
                                                              "threeObjectParameter",
                                                              MethodType.methodType(String.class,
                                                                                    String.class,
                                                                                    String.class,
                                                                                    String.class));

        threePrimateParameterMethod = TestClass.class.getMethod("threePrimateParameter",
                                                                int.class,
                                                                int.class,
                                                                int.class);
        threePrimateParameterMethodHandle = lookup.findVirtual(TestClass.class,
                                                               "threePrimateParameter",
                                                               MethodType.methodType(int.class,
                                                                                     int.class,
                                                                                     int.class,
                                                                                     int.class));

        override1Method1 = TestClass.class.getMethod("override1", String.class);
        override1MethodHandle1 = lookup.findVirtual(TestClass.class,
                                                    "override1",
                                                    MethodType.methodType(String.class, String.class));

        override1Method2 = TestClass.class.getMethod("override1", String.class, String.class);
        override1MethodHandle2 = lookup.findVirtual(TestClass.class,
                                                    "override1",
                                                    MethodType.methodType(String.class, String.class, String.class));

        override1Method3 = TestClass.class.getMethod("override1", String.class, String.class, String.class);
        override1MethodHandle3 = lookup.findVirtual(TestClass.class,
                                                    "override1",
                                                    MethodType.methodType(String.class,
                                                                          String.class,
                                                                          String.class,
                                                                          String.class));

        genericMethod = TestClass.class.getMethod("generic", Map.class);
        genericMethodHandle = lookup.findVirtual(TestClass.class,
                                                 "generic",
                                                 MethodType.methodType(Map.class, Map.class));

        interfaceInstance = new TestInterfaceImpl();
        oneParameterInterfaceMethod = TestInterface.class.getMethod("oneParameterInterfaceMethod", String.class);
        oneParameterInterfaceMethodHandle = lookup.findVirtual(TestInterface.class,
                                                               "oneParameterInterfaceMethod",
                                                               MethodType.methodType(String.class, String.class));
    }

    @Benchmark
    public void noParameterDirectCall() {
        instance.noParameter();
    }

    @Benchmark
    public void noParameterTestReflectionCall() throws Exception {
        noParameterTestMethod.invoke(instance);
    }

    @Benchmark
    public void noParameterMethodHandleCall() throws Throwable {
        noParameterTestMethodHandle.invoke(instance);
    }

    @Benchmark
    public void oneObjectParameterDirectCall() {
        instance.oneObjectParameter("a");
    }

    @Benchmark
    public void oneObjectParameterReflectionCall() throws Exception {
        oneObjectParameterMethod.invoke(instance, "a");
    }

    @Benchmark
    public void oneObjectParameterMethodHandleCall() throws Throwable {
        oneObjectParameterMethodHandle.invoke(instance, "a");
    }

    @Benchmark
    public void onePrimateParameterDirectCall() {
        instance.onePrimateParameter(1);
    }

    @Benchmark
    public void onePrimateParameterReflectionCall() throws Exception {
        onePrimateParameterMethod.invoke(instance, 1);
    }

    @Benchmark
    public void onePrimateParameterMethodHandleCall() throws Throwable {
        onePrimateParameterMethodHandle.invoke(instance, 1);
    }

    @Benchmark
    public void twoObjectParameterDirectCall() {
        instance.twoObjectParameter("a", "b");
    }

    @Benchmark
    public void twoObjectParameterReflectionCall() throws Exception {
        twoObjectParameterMethod.invoke(instance, "a", "b");
    }

    @Benchmark
    public void twoObjectParameterMethodHandleCall() throws Throwable {
        twoObjectParameterMethodHandle.invoke(instance, "a", "b");
    }

    @Benchmark
    public void twoPrimateParameterDirectCall() {
        instance.twoPrimateParameter(1, 2);
    }

    @Benchmark
    public void twoPrimateParameterReflectionCall() throws Exception {
        twoPrimateParameterMethod.invoke(instance, 1, 2);
    }

    @Benchmark
    public void twoPrimateParameterMethodHandleCall() throws Throwable {
        twoPrimateParameterMethodHandle.invoke(instance, 1, 2);
    }

    @Benchmark
    public void threeObjectParameterDirectCall() {
        instance.threeObjectParameter("a", "b", "c");
    }

    @Benchmark
    public void threeObjectParameterReflectionCall() throws Exception {
        threeObjectParameterMethod.invoke(instance, "a", "b", "c");
    }

    @Benchmark
    public void threeObjectParameterMethodHandleCall() throws Throwable {
        threeObjectParameterMethodHandle.invoke(instance, "a", "b", "c");
    }

    @Benchmark
    public void threePrimateParameterDirectCall() {
        instance.threePrimateParameter(1, 2, 3);
    }

    @Benchmark
    public void threePrimateParameterReflectionCall() throws Exception {
        threePrimateParameterMethod.invoke(instance, 1, 2, 3);
    }

    @Benchmark
    public void threePrimateParameterMethodHandleCall() throws Throwable {
        threePrimateParameterMethodHandle.invoke(instance, 1, 2, 3);
    }

    @Benchmark
    public void override1DirectCall1() {
        instance.override1("a");
    }

    @Benchmark
    public void override1ReflectionCall1() throws Exception {
        override1Method1.invoke(instance, "a");
    }

    @Benchmark
    public void override1MethodHandleCall1() throws Throwable {
        override1MethodHandle1.invoke(instance, "a");
    }

    @Benchmark
    public void override1DirectCall2() {
        instance.override1("a", "b");
    }

    @Benchmark
    public void override1ReflectionCall2() throws Exception {
        override1Method2.invoke(instance, "a", "b");
    }

    @Benchmark
    public void override1MethodHandleCall2() throws Throwable {
        override1MethodHandle2.invoke(instance, "a", "b");
    }

    @Benchmark
    public void override1DirectCall3() {
        instance.override1("a", "b", "c");
    }

    @Benchmark
    public void override1ReflectionCall3() throws Exception {
        override1Method3.invoke(instance, "a", "b", "c");
    }

    @Benchmark
    public void override1MethodHandleCall3() throws Throwable {
        override1MethodHandle3.invoke(instance, "a", "b", "c");
    }

    @Benchmark
    public void genericDirectCall() {
        instance.generic(map);
    }

    @Benchmark
    public void genericReflectionCall() throws Exception {
        genericMethod.invoke(instance, map);
    }

    @Benchmark
    public void genericMethodHandleCall() throws Throwable {
        genericMethodHandle.invoke(instance, map);
    }

    @Benchmark
    public void oneParameterInterfaceMethodDirectCall() {
        interfaceInstance.oneParameterInterfaceMethod("a");
    }

    @Benchmark
    public void oneParameterInterfaceMethodReflectionCall() throws Exception {
        oneParameterInterfaceMethod.invoke(interfaceInstance, "a");
    }

    @Benchmark
    public void oneParameterInterfaceMethodMethodHandleCall() throws Throwable {
        oneParameterInterfaceMethodHandle.invoke(interfaceInstance, "a");
    }

    public static void main(String[] args) throws RunnerException {
        Options opt = new OptionsBuilder()
                .include(ReflectTest.class.getSimpleName())
                .build();
        new Runner(opt).run();
    }
}
